package bancosys.tec.persist.uniquevalue.listener;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import jmine.tec.annotations.Unmodifiable;
import jmine.tec.persist.PersistMessages;
import jmine.tec.utils.Tuple;
import jmine.tec.utils.reflection.AnnotationUtils;

import org.apache.commons.beanutils.PropertyUtils;
import org.hibernate.Hibernate;
import org.hibernate.LockMode;
import org.hibernate.event.SaveOrUpdateEvent;
import org.hibernate.event.def.DefaultSaveOrUpdateEventListener;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;

import bancosys.tec.persist.Persistable;
import bancosys.tec.persist.controller.TransactionalController;
import bancosys.tec.persist.dao.BaseDAO;
import bancosys.tec.persist.dao.BeanNotFoundException;
import bancosys.tec.persist.dao.SystemEntityNotFoundException;
import bancosys.tec.persist.exception.PersistenceException;
import bancosys.tec.persist.uniquevalue.annotation.UniqueGeneratedValue;
import bancosys.tec.persist.uniquevalue.domain.UniqueValueReference;

/**
 * Hibernate event listener que gera os valores em campos que possuem a anotação {@link UniqueGeneratedValue}.
 * 
 * @author lundberg
 * @author Rafael Volpato
 */
public final class UniqueGeneratedValueEventListener extends DefaultSaveOrUpdateEventListener implements BeanFactoryAware {

    private static final long DEFAULT_RESERVED_RANGE = 200L;

    private final Map<Class<?>, Collection<FieldMetaData>> cache = new ConcurrentHashMap<Class<?>, Collection<FieldMetaData>>();

    private BeanFactory beanFactory;

    private String transactionalControllerBeanName;

    private TransactionalController controller;

    private ConcurrentMap<String, UniqueValueGenerator> generators = new ConcurrentHashMap<String, UniqueValueGenerator>();

    /**
     * Invalidates all memory alocated ranges. Might be needed if database was altered manualy and ranges became invalid (test cases mostly)
     */
    public void invalidateReservedRanges() {
        this.generators.clear();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onSaveOrUpdate(SaveOrUpdateEvent event) {
        // Os UniqueGeneratedValue devem ser setados somente no insert, nunca no update
        if (this.isInsertAction(event)) {
            // Setar os UniqueGeneratedValue se necessario.
            this.setUniqueGeneratedValues(event);
        }

        super.onSaveOrUpdate(event);
    }

    /**
     * Verifica se o objeto que esta sendo salvo esta sendo incluido ou atualizado. Essa verificação é feita a partir da existencia ou não
     * do ID no objeto.
     * 
     * @param event {@link SaveOrUpdateEvent}
     * @return <code>true</code> se o objeto esta sendo incluido, <code>false</code> se ele estiver sendo atualizado.
     */
    private boolean isInsertAction(SaveOrUpdateEvent event) {
        if (!(event.getObject() instanceof Persistable)) {
            return false;
        }

        Persistable persistable = (Persistable) event.getObject();
        return (persistable.getPk() == null);
    }

    /**
     * Antes de inserir a entidade, define o valor dos campos anotados com UniqueGeneratedValue
     * 
     * @param event event
     */
    private void setUniqueGeneratedValues(SaveOrUpdateEvent event) {
        Class<?> beanClass = Hibernate.getClass(event.getObject());
        Collection<FieldMetaData> fields = this.findMethodsToUpdateValue(beanClass);
        if (fields.isEmpty()) {
            return;
        }

        for (FieldMetaData field : fields) {
            if (this.shouldUpdateUniqueValue(event, field)) {
                this.updateUniqueValue(event, field.getWriteMethod(), field.getUniqueGeneratedValue());
            }
        }
    }

    /**
     * Busca todos os campos da classe que possuem a anotação {@link UniqueGeneratedValue}.
     * 
     * @param clazz clazz
     * @return Metadados de todos os campos da classe que possuem a anotação {@link UniqueGeneratedValue}
     */
    private Collection<FieldMetaData> findMethodsToUpdateValue(Class<?> clazz) {
        // Verificar se essa classe ja foi utilizada alguma vez
        Collection<FieldMetaData> fieldsFromCache = this.cache.get(clazz);
        if (fieldsFromCache != null) {
            return fieldsFromCache;
        }

        // Extrair quais são os campos que devem ser atualizados
        Collection<Tuple<Method, UniqueGeneratedValue>> tuples =
                AnnotationUtils.findMethodsWithAnnotation(clazz, UniqueGeneratedValue.class);

        Collection<FieldMetaData> fields = new LinkedList<FieldMetaData>();
        for (Tuple<Method, UniqueGeneratedValue> tuple : tuples) {
            Method writeMethod = tuple.head();
            Method readMethod = null;

            // Obter o methodo de leitura a partir do metodo de escrita
            PropertyDescriptor[] propertyDescriptors = PropertyUtils.getPropertyDescriptors(clazz);
            for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
                if ((propertyDescriptor.getWriteMethod() != null) && (propertyDescriptor.getWriteMethod().equals(writeMethod))) {
                    readMethod = propertyDescriptor.getReadMethod();
                    break;
                }
            }

            if (readMethod == null) {
                throw new PersistenceException(PersistMessages.GETTER_METHOD_NOT_FOUND.create(writeMethod));
            }

            // Criar uma nova instancia de metadados
            FieldMetaData fieldMetaData = new FieldMetaData(tuple.tail(), readMethod, writeMethod);
            fields.add(fieldMetaData);
        }

        // Salvar os dados no cache.
        this.cache.put(clazz, fields);

        return fields;
    }

    /**
     * Verifica se deve o campo associado ao metodo de escrita deve ter o valor atribuido ou não.
     * 
     * @param event {@link SaveOrUpdateEvent}
     * @param field {@link FieldMetaData}
     * @return <code>true</code> se o campo associado ao metodo de escrita deve ser atualizado, <code>false</code> caso contrario.
     */
    private boolean shouldUpdateUniqueValue(SaveOrUpdateEvent event, FieldMetaData field) {
        // Verificar se o UniqueGeneratedValue ja possui um valor associado

        // Obtem o valor associado ao metodo.
        try {
            Object result = field.getReadMethod().invoke(event.getObject());
            return (result == null);
        } catch (IllegalArgumentException e) {
            throw new PersistenceException(PersistMessages.ERROR_READING_UNIQUE_VALUE.create(), e);
        } catch (IllegalAccessException e) {
            throw new PersistenceException(PersistMessages.ERROR_READING_UNIQUE_VALUE.create(), e);
        } catch (InvocationTargetException e) {
            throw new PersistenceException(PersistMessages.ERROR_READING_UNIQUE_VALUE.create(), e.getTargetException());
        }
    }

    /**
     * Atualiza o UniqueGeneratedValue do objeto
     * 
     * @param event event
     * @param writeMethod method
     * @param ugv {@link UniqueGeneratedValue}
     */
    private void updateUniqueValue(SaveOrUpdateEvent event, Method writeMethod, UniqueGeneratedValue ugv) {
        String mnemonico = ugv.value();
        Long nextValue = this.getNextUniqueValue(mnemonico);

        // Atualizar o objeto
        try {
            writeMethod.invoke(event.getObject(), nextValue);
        } catch (IllegalArgumentException e) {
            throw new PersistenceException(PersistMessages.ERROR_SETTING_UNIQUE_VALUE.create(), e);
        } catch (IllegalAccessException e) {
            throw new PersistenceException(PersistMessages.ERROR_SETTING_UNIQUE_VALUE.create(), e);
        } catch (InvocationTargetException e) {
            throw new PersistenceException(PersistMessages.ERROR_SETTING_UNIQUE_VALUE.create(), e.getTargetException());
        }
    }

    /**
     * @param mnemonico o mnemonico da sequence.
     * @return o próximo valor gerado único de uma sequence.
     */
    public Long getNextUniqueValue(String mnemonico) {
        return this.findOrCreateUniqueValueGenerator(mnemonico).getNextUniqueValue();
    }

    /**
     * Finds or create the UniqueValueGenerator for this mnemonic
     * 
     * @param mnemonico mnemonico
     * @return generator
     */
    private UniqueValueGenerator findOrCreateUniqueValueGenerator(String mnemonico) {
        UniqueValueGenerator generator = this.generators.get(mnemonico);
        if (generator == null) {
            this.generators.putIfAbsent(mnemonico, new UniqueValueGenerator(this.getController(), mnemonico));
            return this.generators.get(mnemonico);
        }
        return generator;
    }

    /**
     * @return the {@link TransactionalController}
     */
    private TransactionalController getController() {
        if (this.controller == null) {
            this.controller = (TransactionalController) this.beanFactory.getBean(this.transactionalControllerBeanName);
        }
        return this.controller;
    }

    /**
     * @param beanFactory the beanFactory to set
     */
    public void setBeanFactory(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    /**
     * @param transactionalControllerBeanName the transactionalControllerBeanName to set
     */
    public void setTransactionalControllerBeanName(String transactionalControllerBeanName) {
        this.transactionalControllerBeanName = transactionalControllerBeanName;
    }

    /**
     * @author lundberg
     */
    private static final class UniqueValueGenerator {

        private final TransactionalController controller;

        private final BaseDAO<UniqueValueReference> dao;

        private final String mnemonico;

        private volatile long nextValue = 0L;

        private volatile long maxReservedValue = -1;

        /**
         * Constructor
         * 
         * @param controller controller
         * @param mnemonico mnemonico
         */
        private UniqueValueGenerator(TransactionalController controller, String mnemonico) {
            this.dao = controller.getDAOFactory().getGenericDAO(UniqueValueReference.class);
            this.controller = controller;
            this.mnemonico = mnemonico;
        }

        /**
         * Gets the next unique value
         * 
         * @return Long
         */
        public synchronized long getNextUniqueValue() {
            long next = this.nextValue++;
            if (next < this.maxReservedValue) {
                return next;
            }
            return this.getAndReserveNextRange();
        }

        /**
         * Reserves the next range
         * 
         * @return next value
         */
        private long getAndReserveNextRange() {
            // Iniciando nova transação para evitar que ocorra um flush dentro de outro.
            this.controller.startTransaction();
            boolean commit = false;
            try {
                UniqueValueReference urv = this.dao.findByNaturalKey(this.mnemonico);
                this.controller.getSessionFactory().getCurrentSession().refresh(urv, LockMode.UPGRADE);
                this.nextValue = urv.getNextVal();
                this.maxReservedValue = this.nextValue + DEFAULT_RESERVED_RANGE;
                urv.setNextVal(this.maxReservedValue);
                urv.getPersister().save();
                commit = true;
                return this.nextValue++;
            } catch (BeanNotFoundException e) {
                throw new SystemEntityNotFoundException(PersistMessages.SYSTEM_ENTITY_NOT_FOUND.create(
                        UniqueValueReference.class.getName(), this.mnemonico), e);
            } finally {
                if (commit) {
                    this.controller.commit();
                } else {
                    this.controller.rollback();
                }
            }
        }

    }

    /**
     * @author Rafael Volpato
     */
    @Unmodifiable
    private static class FieldMetaData {

        private final UniqueGeneratedValue uniqueGeneratedValue;

        private final Method writeMethod;

        private final Method readMethod;

        /**
         * Construtor.
         * 
         * @param uniqueGeneratedValue uniqueGeneratedValue
         * @param readMethod readMethod
         * @param writeMethod writeMethod
         */
        public FieldMetaData(UniqueGeneratedValue uniqueGeneratedValue, Method readMethod, Method writeMethod) {
            this.uniqueGeneratedValue = uniqueGeneratedValue;
            this.readMethod = readMethod;
            this.writeMethod = writeMethod;
        }

        /**
         * @return the uniqueGeneratedValue
         */
        public final UniqueGeneratedValue getUniqueGeneratedValue() {
            return this.uniqueGeneratedValue;
        }

        /**
         * @return the writeMethod
         */
        public final Method getWriteMethod() {
            return this.writeMethod;
        }

        /**
         * @return the readMethod
         */
        public final Method getReadMethod() {
            return this.readMethod;
        }
    }
}
